EC2でAnsible練習環境を作成してみた
データアナリティクス事業本部の鈴木です。
EC2を対象に、Ansibleでサーバー構築をする機会がありました。既存の設定を使うだけではなく、自分でもいろいろ動かして勉強してみたかったので、練習環境を構築するためのCloudFormationテンプレートと、簡単なプレイブックを作成したのでご紹介します。
やりたかったこと
この記事では、以下を試してみました。
- Ansibleで環境構築を行うためのEC2インスタンスおよび必要なリソース(VPCやエンドポイントをはじめとしたネットワークリソースなど)を作成する。
- AnsibleでEC2インスタンスにソフトウェアをインストールする。
Ansibleはローカル環境を対象にもできますが、この記事を見ているような方だと、AWS環境上にあるEC2を対象とした場合があるように思います。
その場合、練習に使える出来立てのネットワークとEC2インスタンスがあるといいのですが、そうでない場合は1から作ることになるのでやりたいことに対して多少の手間が発生します。そのため、Ansibleのサンプルの実行に加えて、AWS上の環境構築についても行いました。
前提
Ansibleのインストール
今回はpipにてインストールしました。
pip install ansible ansible --version # ansible [core 2.14.5] # (略)
構築する環境
以下のような構成を構築します。
ポイントとしては以下です。
- NATゲートウェイ経由でインターネットに出られるようにしています。Ansibleでソフトウェアをインストールする際に必要になります。
- Session Managerで接続できるように、必要なエンドポイントとセキュリティグループを作成しています。
- ゲートウェイ型のS3エンドポイントを作成しています。
- EC2インスタンスはAmazon Linux 2023を使用しています。
デプロイしてEC2が起動すれば、Session Managerを使って接続できるようにします。
やってみた
1. 構成のデプロイ
CloudFormationから以下のテンプレートをデプロイしました。
EC2のキーペアは先に作成してある想定です。
AWSTemplateFormatVersion: "2010-09-09" Parameters: EnvironmentName: Type: String VPCCIDR: Type: String Default: 10.192.0.0/16 PublicSubnetCIDR: Type: String Default: 10.192.1.0/24 PrivateSubnetCIDR: Type: String Default: 10.192.0.0/24 Ec2ImageId: Type: AWS::SSM::Parameter::Value<String> Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 Ec2InstanceType: Type: String Default: t3.micro KeyPair: Type: String Default: xxxxx-key Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VPCCIDR EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub ${EnvironmentName}-VPC # InternetGateway InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub ${EnvironmentName}-igw AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway NatGateway: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayEIP.AllocationId SubnetId: !Ref PublicSubnet Tags: - Key: Name Value: !Sub ${EnvironmentName}-ngw NatGatewayEIP: Type: AWS::EC2::EIP Properties: Domain: vpc # Public Subnetのネットワーク設定 PublicSubnet: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select [0, !GetAZs ""] CidrBlock: !Ref PublicSubnetCIDR VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PublicSubnet PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PublicRouteTable PublicRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable # Private Subnetのネットワーク設定 PrivateSubnet: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select [0, !GetAZs ""] CidrBlock: !Ref PrivateSubnetCIDR VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PrivateSubnet PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PrivateRouteTable PrivateRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway PrivateSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable # エンドポイントの設定 EndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: EndpointSecurityGroup VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-EndpointSecurityGroup SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: !Ref VPCCIDR EndpointSSM: Type: AWS::EC2::VPCEndpoint Properties: PrivateDnsEnabled: true SecurityGroupIds: - !Ref EndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm SubnetIds: - !Ref PrivateSubnet VpcEndpointType: Interface VpcId: !Ref VPC EndpointSSMMessages: Type: AWS::EC2::VPCEndpoint Properties: PrivateDnsEnabled: true SecurityGroupIds: - !Ref EndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages SubnetIds: - !Ref PrivateSubnet VpcEndpointType: Interface VpcId: !Ref VPC EndpointEC2Messages: Type: AWS::EC2::VPCEndpoint Properties: PrivateDnsEnabled: true SecurityGroupIds: - !Ref EndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages SubnetIds: - !Ref PrivateSubnet VpcEndpointType: Interface VpcId: !Ref VPC EndpointS3: Type: AWS::EC2::VPCEndpoint Properties: RouteTableIds: - !Ref PrivateRouteTable ServiceName: !Sub com.amazonaws.${AWS::Region}.s3 VpcEndpointType: Gateway VpcId: !Ref VPC EC2IAMRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${EnvironmentName}-SSM-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore EC2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - Ref: EC2IAMRole InstanceProfileName: !Sub ${EnvironmentName}-EC2InstanceProfile EC2SecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: EC2SecurityGroup VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-EC2SecurityGroup EC2Instance: Type: AWS::EC2::Instance Properties: InstanceType: !Ref Ec2InstanceType SubnetId: !Ref PrivateSubnet ImageId: !Ref Ec2ImageId SecurityGroupIds: - !Ref EC2SecurityGroup IamInstanceProfile: !Ref EC2InstanceProfile BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeSize: 50 VolumeType: gp3 EbsOptimized: true SourceDestCheck: true KeyName: !Ref KeyPair Tags: - Key: Name Value: !Sub ${EnvironmentName}-EC2Instance Outputs: VPC: Value: !Ref VPC Export: Name: !Sub ${EnvironmentName}-VPC VPCCIDR: Value: !Ref VPCCIDR Export: Name: !Sub ${EnvironmentName}-VPCCIDR PrivateSubnet: Value: !Ref PrivateSubnet Export: Name: !Sub ${EnvironmentName}-PrivateSubnet PrivateRouteTable: Value: !Ref PrivateRouteTable Export: Name: !Sub ${EnvironmentName}-PrivateRouteTable SecurityGroup: Value: !Ref EndpointSecurityGroup Export: Name: !Sub ${EnvironmentName}-EndpointSecurityGroup EndpointSSM: Value: !Ref EndpointSSM Export: Name: !Sub ${EnvironmentName}-EndpointSSM EndpointSSMMessages: Value: !Ref EndpointSSMMessages Export: Name: !Sub ${EnvironmentName}-EndpointSSMMessages EndpointEC2Messages: Value: !Ref EndpointEC2Messages Export: Name: !Sub ${EnvironmentName}-EndpointEC2Messages EndpointS3: Value: !Ref EndpointS3 Export: Name: !Sub ${EnvironmentName}-EndpointS3 EC2SecurityGroup: Value: !Ref EC2SecurityGroup Export: Name: !Sub ${EnvironmentName}-EC2SecurityGroup EC2Instance: Value: !Ref EC2Instance Export: Name: !Sub ${EnvironmentName}-EC2Instance
テンプレートは以下のブログを参考にしました。
CloudFormationでスタックがCREATE_COMPLETE
になることを確認しました。
2. EC2インスタンスへのSSH接続確認
今回デプロイしたEC2インスタンスはセッションマネージャー経由でSSH接続できるので、本当に接続できるか確認しました。
.ssh/config
に以下を追記しました。ただし、インスタンスID
・プロファイル名
・秘密鍵のパス
は作成したインスタンスやローカル環境に設定しているものに置き換えました。
host cm-nayuts-ansible-sample ProxyCommand sh -c "aws ssm start-session --target <インスタンスID> --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --profile <プロファイル名> --region ap-northeast-1" User ec2-user IdentityFile <秘密鍵のパス>
sshコマンドで接続すると、ログインできました。
ssh cm-nayuts-ansible-sample # , #_ # ~\_ ####_ Amazon Linux 2023 # ~~ \_#####\ # ~~ \###| # ~~ \#/ ___ https://aws.amazon.com/linux/amazon-linux-2023 # ~~ V~' '-> # ~~~ / # ~~._. _/ # _/ _/ # _/m/' # Last login: Sat May 20 05:52:40 2023 from 127.0.0.1 [ec2-user@ip-<ローカルのアドレス> ~]$
3. Ansible用のファイルの作成
ansible
ディレクトリを作成し、以下のようにファイルを作成しました。
tree ansible # . # ├── ansible.cfg # ├── hosts # ├── playbook.yml # └── ssh_config
ファイル名などは以下のブログを参考にしました。
ansible.cfgファイルの作成
ansible.cfg
は以下のようにしました。
[defaults] inventory = hosts [privilege_escalation] become = True [ssh_connection] control_path = %(directory)s/%%h-%%r ssh_args = -o ControlPersist=15m -F ssh_config -q scp_if_ssh = True
特に、ansible.cfg
の内容はAnsible Configuration Settingsで最新の仕様を確認しました。
ssh_args
はman ssh
でsshコマンドのオプションの内容を確認しました。特に、-F ssh_config
でSSHの設定をファイルに外出ししました。
ssh_configファイルの作成
SSH接続の情報を記載するファイルです。
先にSSH接続可否を確認した際に、.ssh/config
に追記した内容と同じものを記載しました。
host cm-nayuts-ansible-sample ProxyCommand sh -c "aws ssm start-session --target <インスタンスID> --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --profile <プロファイル名> --region ap-northeast-1" User ec2-user IdentityFile <SSH鍵のパス>
hostsファイルの作成
ansibleのインベントリです。
.iniファイルの形式で以下のように記載しました。
[cm-nayuts-sample] cm-nayuts-ansible-sample
インベントリの書き方はドキュメントの以下のページが参考になりました。
playbook.ymlファイルの作成
自動化する内容を記載するファイルです。
- hosts: cm-nayuts-sample tasks: - name: Ensure jq is at the latest version ansible.builtin.dnf: name: jq state: latest
プレイブックの書き方として、ドキュメントの以下のページを参考にしました。
また、Taskで使用したansible.builtin.dnf
モジュールを使ってjq
をインストールしてみました。
記載する設定は以下のドキュメントを参考に書いてみました。
今回はAmazon Linux 2023で試していますが、Amazon Linux 2023がGAされましたでパッケージマネージャーがdnf
に変更されていることと、Amazon Linux 2023 packages updated 2023-05-01にjqがあることを確認したので、そのような設定としました。
なお、インスタンスにjq
がインストールされているか確認しましたが、あらかじめインストールされていませんでした。プレイブックの実行後にインストールされていれば、正常にAnsibleからインストールできたことが分かりやすいですね。
[ec2-user@ip-<ローカルのアドレス> ~]$ which jq # /usr/bin/which: no jq in (/home/ec2-user/.local/bin:/home/ec2-user/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin)
4. プレイブックの実行
最後に実際にプレイブックを実行してみました。
ローカルで以下のコマンドを実行しました。
[ec2-user@ip-<ローカルのアドレス> ~]$ ansible-playbook ./playbook.yml # [WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details # # PLAY [cm-nayuts-sample] ************************************************************************************************ # # TASK [Gathering Facts] ************************************************************************************************* # [WARNING]: Platform linux on host cm-nayuts-ansible-sample is using the discovered Python interpreter at # /usr/bin/python3.9, but future installation of another Python interpreter could change the meaning of that path. See # https://docs.ansible.com/ansible-core/2.14/reference_appendices/interpreter_discovery.html for more information. # ok: [cm-nayuts-ansible-sample] # # TASK [Ensure jq is at the latest version] ****************************************************************************** # changed: [cm-nayuts-ansible-sample] # PLAY RECAP ************************************************************************************************************* # cm-nayuts-ansible-sample : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
jq
のインストールが無事に完了しました。
Pythonインタプリタに関する警告が出ていますが、対処方法は以下のブログに紹介がありました。今回は特に気にしないこととします。
SSHログインしてみると、確かにjq
がインストールされていることを確認できました。
[ec2-user@ip-<ローカルのアドレス> ~]$ which jq # /usr/bin/jq
5. 片付け
作成したCloudFormationスタックを削除することで、検証用のリソースは削除できます。特にNATゲートウェイなど処理データ量に加えて時間単位でも課金が発生するものがあるため、検証目的の場合は終わったら削除しておきましょう。
最後に
今回はAnsibleを使って、Amazon Linux 2023にソフトウェアをインストールする例を紹介しました。
デプロイするだけでセッションマネージャーですぐにログインできるEC2インスタンスをVPCからまとめて作成できるCloudFormationテンプレートを作成したので、いつでもAnsibleの練習ができますね!
参考になりましたら幸いです。